#include "hdi.h"

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>

/// Returns the memory adress of the block id on the given virtual drive,
/// or NULL if out of range.
static byte_t* fs_getBlockMemoryAdress(HardDriveInterface_t* hdi,blockid_t id){
	if(id>=hdi->blockCount){
		return NULL;
	}
	if(hdi->diskBlocksContinuous){
		return (byte_t*)(hdi->diskBlocksContinuous+id*hdi->blockSize);
	}else{
		return hdi->diskBlocksDiscontinuous[id];
	}
}

fs_ret_t fs_newVirtualHDI(HardDriveInterface_t* hdi,uint32_t blockCount,uint16_t blockSize){
	hdi->blockSize = blockSize;
	hdi->blockCount = blockCount;
	// Allocating the disk. 2 strategies :
	// - Allocate a contiguous space, to avoid memory fragmentation, and get a faster access.
	// - Allocate a discontinuous space, if the first fail.
	// To determinate which of these is used, we test if the first pointer is NULL.
	hdi->diskBlocksContinuous = malloc(blockCount*blockSize*sizeof(byte_t));
	if(hdi->diskBlocksContinuous){
		hdi->diskBlocksDiscontinuous = NULL;
	}else{// Allocating the big chunk failed, trying with smaller chunks.
		uint32_t blockToAllocate,blockToFree;
		char mallocError=0;
		hdi->diskBlocksDiscontinuous = malloc(blockCount*sizeof(byte_t*));
		if(!hdi->diskBlocksDiscontinuous){
			free(hdi);
			return ERR_OUT_OF_MEMORY;
		}
		// Allocating the blocks
		for(blockToAllocate=0; blockToAllocate<blockCount && !mallocError; ++blockToAllocate){
			hdi->diskBlocksDiscontinuous[blockToAllocate] = malloc(blockSize*sizeof(byte_t));
			if(!hdi->diskBlocksDiscontinuous[blockToAllocate]){
				mallocError = 1;
			}
		}
		if(mallocError){
			for(blockToFree=blockToAllocate-1;blockToFree<=0;--blockToFree){
				free(hdi->diskBlocksDiscontinuous[blockToFree]);
			}
			free(hdi->diskBlocksDiscontinuous);
			free(hdi);
			return ERR_OUT_OF_MEMORY;
		}
	}
	return RET_NO_PROBLEM;
}


fs_ret_t fs_deleteVirtualHDI(HardDriveInterface_t* hdi){
	if(hdi->diskBlocksContinuous){
		free(hdi->diskBlocksContinuous);
	}else{//The discountinuous version is used.
		blockid_t blockToFree;
		for(blockToFree=0;blockToFree<hdi->blockCount;++blockToFree){
			free(hdi->diskBlocksDiscontinuous[blockToFree]);
		}
		free(hdi->diskBlocksDiscontinuous);
	}
	return RET_NO_PROBLEM;
}

fs_ret_t fs_saveHDIToHardDrive(HardDriveInterface_t* hdi, const char* fileToSaveAs){
	blockid_t currentBlockIndex;
	byte_t *block = malloc(hdi->blockSize);
	fs_ret_t ret = RET_NO_PROBLEM;
	FILE* file = fopen(fileToSaveAs, "wb");
	if(!file){
		ret = ERR_EXTERNAL_LIB_PROBLEM;
		goto cleanup;
	}
	if(!block) return ERR_OUT_OF_MEMORY;

	if(fwrite((void*)(&(hdi->blockCount)),4,1,file)!=1){
		ret = ERR_EXTERNAL_LIB_PROBLEM;
		goto cleanup;
	}
	if(fwrite((void*)(&(hdi->blockSize)),2,1,file)!=1){
		ret = ERR_EXTERNAL_LIB_PROBLEM;
		goto cleanup;
	}

	for(currentBlockIndex = 0;currentBlockIndex<hdi->blockCount;++currentBlockIndex){
		ret = fs_getBlockFromHdi(hdi,currentBlockIndex, block);
		if(ret) goto cleanup;
		if(fwrite((void*)block, hdi->blockSize, 1,file)!=1){
			ret = ERR_EXTERNAL_LIB_PROBLEM;
			goto cleanup;
		}
	}
	cleanup:
	if(file){
		fclose(file);
	}
	free(block);
	return ret;
}

fs_ret_t fs_loadHDIFromHardDrive(HardDriveInterface_t* hdi, const char* fileToLoad){
	uint32_t currentBlockIndex;
	byte_t *block = NULL;
	fs_ret_t ret = RET_NO_PROBLEM;
	uint32_t blockCount = 0;
	uint16_t blockSize = 0;
	bool hdiAllocated = false;
	FILE* file = fopen(fileToLoad, "rb");
	if(!file){
		ret = ERR_EXTERNAL_LIB_PROBLEM;
		goto cleanup;
	}
	if(fread((void*)(&blockCount),4,1,file)!=1){
		ret = ERR_EXTERNAL_LIB_PROBLEM;
		goto cleanup;
	}
	if(fread((void*)(&(blockSize)),2,1,file)!=1){
		ret = ERR_EXTERNAL_LIB_PROBLEM;
		goto cleanup;
	}
	ret = fs_newVirtualHDI(hdi,blockCount,blockSize);
	if(ret) goto cleanup;
	hdiAllocated = true;
	block = malloc(hdi->blockSize);
	if(!block){
		ret = ERR_OUT_OF_MEMORY;
		goto cleanup;
	}
	for(currentBlockIndex = 0;currentBlockIndex<hdi->blockCount;++currentBlockIndex){
		if(fread((void*)block, 1, hdi->blockSize,file)!=hdi->blockSize){
			ret = ERR_EXTERNAL_LIB_PROBLEM;
			goto cleanup;
		}
		ret = fs_setBlockToHdi(hdi,currentBlockIndex, block);
		if(ret) goto cleanup;
	}
	cleanup:
	if(file){
		fclose(file);
	}
	if(ret && hdiAllocated){
		fs_deleteVirtualHDI(hdi);
		// TODO : find a error to send if fs_deleteVirtualHDI fails.
	}
	free(block);
	return ret;
}

fs_ret_t fs_getBlockFromHdi(HardDriveInterface_t* hdi,blockid_t id, byte_t * const dest){
	byte_t *from = fs_getBlockMemoryAdress(hdi,id);
	if(from==NULL)
		return ERR_CANT_DO_IT_ALWAYS;
	memcpy(dest,from,hdi->blockSize);
	return RET_NO_PROBLEM;
}

fs_ret_t fs_setBlockToHdi(HardDriveInterface_t* hdi,blockid_t id,const byte_t * from){
	byte_t *dest = fs_getBlockMemoryAdress(hdi,id);
	if(dest==NULL)
		return ERR_CANT_DO_IT_ALWAYS;
	memcpy(dest,from,hdi->blockSize);
	return RET_NO_PROBLEM;
}

/// Prints a byte in binary.
static void print_bin(byte_t c){
	int8_t i;
	for(i=7;i>=0;--i) printf("%d",(c&(1<<i))>>i);
}

fs_ret_t fs_d_print_block(HardDriveInterface_t* hdi,blockid_t blockId){
	byte_t *block= malloc(hdi->blockSize);
	uint32_t cpt;
	fs_ret_t ret_getBlockFromHdi = fs_getBlockFromHdi(hdi,blockId,block);
	if(!block){
		return ERR_OUT_OF_MEMORY;
	}
	if(ret_getBlockFromHdi){
		WARNING_PRINT("Could not print the block %d, he is probably out of range.\n",blockId);
		free(block);
		return ret_getBlockFromHdi;
	}
	printf("----------------------\n");
	printf("| block : 0x%08x |\n",blockId);
	printf("----------------------\n");
	printf("Adress | VBinary  | Vuint32   | VChar\n");
	for(cpt=0;cpt<hdi->blockSize;++cpt){
		printf("0x%04x | ",cpt);
		print_bin(block[cpt]);
		printf(" | ");
		if(!(cpt%4)){
			printf("%09u",(uint32_t)(block[cpt]));
		}else{
			printf("         ");
		}
		printf(" | ");
		if(isprint(block[cpt])&&block[cpt]!='\n'){
			printf("%c",block[cpt]);
		}else if(block[cpt]=='\n'){
			printf("\\n");
		}
		printf("\n");
	}
	free(block);
	return RET_NO_PROBLEM;
}

